package server.networking;

/**
 * The ServerRemote class serves as the implementation of the
 * ServerRemoteInterface interface. Clients that retrieve the ServerRemote
 * stub have access to any of the methods available in this class.
 */

import java.rmi.RemoteException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;

import model.Player;
import model.Table;
import model.TableInformation;
import model.TableSettings;
import model.user.User;
import model.user.User.Gender;
import model.user.UserBank;
import model.user.UserSettings;
import model.user.UserStatistics;
import client.networking.ClientCallbackInterface;

public class ServerRemote implements ServerRemoteInterface {
	/**
	 * A hash map of the tables hosted on the server using the table's ID as a key.
	 */
	private HashMap<Integer, Table> tables;
	
	/**
	 * A hash map of the users currently connected to the server using the user's username as a key.
	 */
	private HashMap<String, User> connectedUsers;
	
	/**
	 * A hash map of the ClientCallback stubs left behind by the clients that connect to the server, using the connected client's username as a key.
	 */
	private HashMap<String, ClientCallbackInterface> connectedClients;
	
	/**
	 * The max number of tables allowed to be hosted on the server.
	 */
	private int MAX_TABLES = 100;
	
	/**
	 * Constructor. Initialises the hash maps for connected users, connected clients, and tables hosted.
	 */
	public ServerRemote() {
		connectedUsers = new HashMap<String, User>();
		connectedClients = new HashMap<String, ClientCallbackInterface>();
		tables = new HashMap<Integer, Table>();
	}
	
	/**
	 * When a client connects to the server, it can leave an instance of its callback stub using this class.
	 * @param username The username attached to the client
	 * @param c The callback stub
	 */
	public void addClient(String username, ClientCallbackInterface c) throws RemoteException {
		connectedClients.put(username, c);
	}
	
	/**
	 * Removes the client from the server. Used when a user leaves a table to tell the server to stop broadcasting to it.
	 * @param username The username attached to the client.
	 */
	public void removeClient(String username) throws RemoteException {
		connectedClients.remove(username);
	}
	
	/**
	 * Creates a new account on the server and stores it using SQL.
	 * @param username The desired username.
	 * @param password The desired password
	 * @param userSettings The settings declared by the user at registration.
	 */
	@Override
	public void createAccount(String username, String password, UserSettings userSettings) throws RemoteException {
		
		//Access the SQL Database and create a new account
		try {
			Statement stmt;
			Class.forName("com.mysql.jdbc.Driver").newInstance();
			String url = "jdbc:mysql://instance10905.db.xeround.com:7414/fcsgroup2"; //Location of Database
			
			//Connect to the Database
			Connection con = DriverManager.getConnection(url,"fcsgroup2", "qwerty123");

			System.out.println("URL: " + url);
			System.out.println("Connection: " + con);
			
			//Get a Statement Object
			stmt = con.createStatement();
			
			String gendertemp;
			if(userSettings.getGender().equals(Gender.MALE))
				gendertemp = "M";
			else
				gendertemp = "F";
			
			String update1 = "INSERT INTO users(Username,Password,Email,FirstName,LastName,Age,Location,Gender,"
					+"BankRoll,Rank,HandsPlayed,HandsWon,ExperiencePoints) VALUES('"+username+"','"+password+"','"
					+userSettings.getEmail()+"','"+userSettings.getFirstName()+"','"+userSettings.getLastName()+"',"
					+userSettings.getAge()+",'"+userSettings.getLocation()+"','"+gendertemp+"',1000,1,0,0,0)";
			
			String update2 = "INSERT INTO social(Username) VALUES('"+username+"')";
			
			try {
				stmt.executeUpdate(update1);
				stmt.executeUpdate(update2);
			}
			catch(SQLException e) {
				System.out.println("Registration Failed! Username or Email already exists!");
			}
			
			con.close();
			stmt.close();
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	@Override
	/**
	 * Checks the database to ensure that the username and password are correct
	 * @param username Username to login with
	 * @param password Password to login with
	 */
	public User login(String username, String password) throws RemoteException, SQLException {
		// Checks the database to ensure that the username and password are correct
		// Also, check to ensure that the User isn't already logged in.
		
		//Access sql database and obtain user information
		try {
			Statement stmt;
			ResultSet rs;
			Class.forName("com.mysql.jdbc.Driver").newInstance();
			String url = "jdbc:mysql://instance10905.db.xeround.com:7414/fcsgroup2"; //Location of Database
			
			//Connect to the Database
			Connection con = DriverManager.getConnection(url,"fcsgroup2", "qwerty123");

			System.out.println("URL: " + url);
			System.out.println("Connection: " + con);
			
			//Get a Statement Object
			stmt = con.createStatement();
			
			String update = "SELECT * from users ORDER BY Username";
			rs = stmt.executeQuery(update);
			
			rs.next();
			
			while(!(rs.getString("Username").equals(username)))
				rs.next();
			
			if(password.equals(rs.getString("Password")))
			{
				System.out.println("Login Successful!");
				UserSettings userSettings = new UserSettings(rs.getString("Email"),rs.getString("FirstName"),rs.getString("LastName"),
						Gender.MALE,rs.getInt("Age"),rs.getString("Location"));
				
				//determine gender
				if(rs.getString("Gender").equals("M"))
					userSettings.setGender(Gender.MALE);
				else
					userSettings.setGender(Gender.FEMALE);
				
				//determine ranking
				User.Ranking pRank = User.Ranking.SCRUB;
				int rankInt = rs.getInt("Rank");
				switch(rankInt) {
					case 0: pRank = User.Ranking.STUD; break;
					case 1: pRank = User.Ranking.SCRUB; break;
					case 2: pRank = User.Ranking.NEWBIE; break;
					case 3: pRank = User.Ranking.NOVICE; break;
					case 4: pRank = User.Ranking.AMATEUR; break;
					case 5: pRank = User.Ranking.SEASONED; break;
					case 6: pRank = User.Ranking.SKILLED; break;
					case 7: pRank = User.Ranking.SEMIPRO; break;
					case 8: pRank = User.Ranking.ADEPT; break;
					case 9: pRank = User.Ranking.PRO; break;
					case 10: pRank = User.Ranking.MASTER; break;
					default: break;
				}
				
				UserStatistics userStatistics = new UserStatistics(pRank,rs.getInt("HandsPlayed"),rs.getInt("HandsWon"),
						rs.getInt("ExperiencePoints"));
				
				UserBank userBank = new UserBank(rs.getInt("BankRoll"));
				
				User u = new User(username, password, userSettings, userStatistics, userBank);
				connectedUsers.put(username, u);
				System.out.println("Returning User object...");
				//Close the connection
				con.close();
				stmt.close();
				return u;
			}
				
			else {
				System.out.println("Login Failed");
				//Close the connection
				con.close();
				stmt.close();
				return null;
			}
				
		}
		catch(Exception e) {
			e.printStackTrace();
		}
		return null;
		
	}
	
	/**
	 * Logs the user out of the server by removing the corresponding User object from the hash map.
	 * @param username The username of the user to logout.
	 */
	@Override
	public void logout(String username) throws RemoteException {
		connectedUsers.remove(username);
	}
	
	/**
	 * Creates a table on the server with the table settings specified in the input parameter. Returns the tableID or 0 if the max tables on the server was reached.
	 * @param tableSettings User-specified table settings
	 * @return The tableID or 0, depending on whether or not the max number of tables has been reached.
	 * 
	 */
	@Override
	public int createTable(TableSettings tableSettings) throws RemoteException {
		if(tables.keySet().size() < MAX_TABLES) {
			Table t = new Table(tableSettings);
			tables.put(t.getTableID(), t);
			System.out.println(tables.toString());
			return t.getTableID();
		}
		return 0;
	}

	/**
	 * Attaches the given user to the table with the given tableID, and gives the user the number of chips specified.
	 * @param u The user joining the table
	 * @param tableID The ID of the table the user wishes to join
	 * @param chips The number of chips the user is coming in with
	 * @return The tableID if the table is not full, otherwise 0.
	 */
	@Override
	public int joinTable(User u, int tableID, int chips)throws RemoteException {
		Table t = tables.get(tableID);
		
		if (!t.isFull()) {
			Player p = new Player(u, chips, t);
			t.addPlayer(p);
			//t.updateTableStatus();
			return tableID;
		}
		else {
			return 0;
		}
			
	}

	/**
	 * Removes a player from the table.
	 * @param player The player leaving the table
	 * @param tableID The identifier of the table the player is leaving.
	 */
	@Override
	public void leaveTable(Player player, int tableID) throws RemoteException {
		Table t = tables.get(tableID);
		t.removePlayer(player);
		t.updateTableStatus();
	}

	@Override
	/**
	 * Change the username in the database
	 * @param user The user object containing the account requesting username change
	 * @param newUsername The new username to change to
	 */
	public User modifyUsername(User user, String newUsername) throws RemoteException {
			try {
				Statement stmt;
				Class.forName("com.mysql.jdbc.Driver").newInstance();
				String url = "jdbc:mysql://instance10905.db.xeround.com:7414/fcsgroup2"; //Location of Database
				
				//Connect to the Database
				Connection con = DriverManager.getConnection(url,"fcsgroup2", "qwerty123");

				System.out.println("URL: " + url);
				System.out.println("Connection: " + con);
				
				//Get a Statement Object
				stmt = con.createStatement();
				
				String update1 = "UPDATE users SET Username='"+newUsername+"' WHERE Username='"+user.getUsername()+"'";
				String update2 = "UPDATE social SET Username='"+newUsername+"' WHERE Username='"+user.getUsername()+"'";
				
				try {
					stmt.executeUpdate(update1);
					stmt.executeUpdate(update2);
					user.setUsername(newUsername);
				}
				catch(SQLException se) {
					System.out.println("Username Update Failed! Username already exists!");
				}
				con.close();
				stmt.close();
				return user;
			}
			catch(Exception e) {
				e.printStackTrace();
				return user;
			}
	}
	
	@Override
	/**
	 * Modify the password belonging to the accout
	 * @param user The user object containing the account requesting password change
	 * @param oldPass For checking the old password belonging to the account
	 * @param newPass The password to change to
	 */
	public User modifyPassword(User user, String oldPass, String newPass) throws RemoteException {
		if(oldPass.equals(user.getPassword())){
			try {
				Statement stmt;
				Class.forName("com.mysql.jdbc.Driver").newInstance();
				String url = "jdbc:mysql://instance10905.db.xeround.com:7414/fcsgroup2"; //Location of Database
				
				//Connect to the Database
				Connection con = DriverManager.getConnection(url,"fcsgroup2", "qwerty123");

				System.out.println("URL: " + url);
				System.out.println("Connection: " + con);
				
				//Get a Statement Object
				stmt = con.createStatement();
				
				String update = "UPDATE users SET Password='"+newPass+"' WHERE Username ='"+user.getUsername()+"'";
				stmt.executeUpdate(update);
				user.setPassword(newPass);
								
				stmt.close();
				con.close();
				
				return user;
			}
			catch(Exception e) {
				e.printStackTrace();
				return user;
			}
		}
		else{
			System.out.println("Wrong password!");
			return user;
		}
	}
	
	@Override
	/**
	 * Change the personal information belonging to the account
	 * @param user The user object containing the account requesting personal information changes
	 * @param firstName The new first name to change to
	 * @param lastName The new last name to change to
	 * @param age The new age to change to
	 * @param gender The new Gender (Male or Female) to change to
	 * @param location The new location to change to
	 */
	public User changePersonalInfo(User user, String firstName, String lastName, String age, String gender, String location) throws RemoteException{
		try {
			Statement stmt;
			Class.forName("com.mysql.jdbc.Driver").newInstance();
			String url = "jdbc:mysql://instance10905.db.xeround.com:7414/fcsgroup2"; //Location of Database
			
			//Connect to the Database
			Connection con = DriverManager.getConnection(url,"fcsgroup2", "qwerty123");

			System.out.println("URL: " + url);
			System.out.println("Connection: " + con);
			
			//Get a Statement Object
			stmt = con.createStatement();
			
			String gendertemp;
			Gender genderEnumTemp;
			if(gender.equals("MALE")) {
				gendertemp = "M";
				genderEnumTemp = Gender.MALE;
			}
			else {
				gendertemp = "F";
				genderEnumTemp = Gender.FEMALE;
			}
			
			String update = "UPDATE users SET FirstName='"+firstName+"', LastName='"+lastName+"', Gender='"+gendertemp
					+"', Age="+age+", Location='"+location+"' WHERE Username ='"+user.getUsername()+"'";
			
			stmt.executeUpdate(update);
			stmt.close();
			con.close();
			
			user.getUserSettings().setFirstName(firstName);
			user.getUserSettings().setLastName(lastName);
			user.getUserSettings().setAge(Integer.parseInt(age));
			user.getUserSettings().setLocation(location);
			user.getUserSettings().setGender(genderEnumTemp);
			
			return user;
			
		}
		catch(Exception e) {
			e.printStackTrace();
			return user;
		}
	}
	
	/**
	 * Returns the max number of tables that can be hosted on the server.
	 * @return The max number of tables.
	 */
	public int getMaxTables() throws RemoteException{
		return MAX_TABLES;
	}
	
	/**
	 * Returns the number of tables currently hosted on the server
	 * @return The number of tables currently hosted on the server.
	 */
	public int getNumTables() throws RemoteException{
		return tables.keySet().size();
	}
	
	/**
	 * Returns the list of table identifiers (integers) of tables that are currently hosted on the server.
	 * @return The list of table IDs
	 */
	public ArrayList<Integer> getTableIDs() throws RemoteException{
		ArrayList<Integer> keySet = new ArrayList<Integer>(tables.keySet());
		return keySet;
	}
	
	/**
	 * Returns the number of free slots at the table with the tableID specified.
	 * @param tableID The table identifier
	 * @return The number of free slots
	 */
	public int getTableFreeSlots(int tableID) throws RemoteException{
		return tables.get(tableID).getSlotsAvailable();
	}
	
	/**
	 * Returns the table settings at the table with the tableID specified.
	 * @param tableID The table identifier
	 * @return The table settings
	 */
	public TableSettings getTableSettings(int tableID) throws RemoteException{
		return tables.get(tableID).getTableSettings();
	}
	
	// Gameplay methods
	
	/**
	 * Returns the username of the player sitting at a specified seat at a specified table.
	 * @param seat The seat at the table
	 * @param tableID The table identifier
	 * @return The username as a string
	 */
	@Override
	public String getPlayerAt(int tableID, int seat) throws RemoteException{
		Table t = tables.get(tableID);
		return t.getPlayerName(seat);		
	}
	
	/**
	 * Adds a player to the table specified with the tableID and gives him a specified amount of chips.
	 * @param tableID The table identifier
	 * @param username The username of the user wishing to join the table
	 * @param chips The number of chips the user is bringing to the table
	 */
	@Override
	public void addPlayerToTable(int tableID, String username, int chips) throws RemoteException{
		User user = connectedUsers.get(username);
		Table t = tables.get(tableID);
		t.addPlayer(new Player(user, chips, t));
		
		for (int i = 0; i < t.getPlayers().size(); i++) {
			connectedClients.get(t.getPlayers().get(i).getName()).refreshTable(tableID);
		}
	}
	
	
	/**
	 * Starts the first street at the table specified.
	 * @param tableID The table identifier
	 */
	@Override
	public void startFirstStreet(int tableID)throws RemoteException{
		Table t = tables.get(tableID);
		t.setStreetCount(1);
		t.initialiseHand();
		t.setCallCount(0);
		determineFirstBetter(tableID, true);
		
		for (int i = 0; i < t.getPlayers().size(); i++) {
			connectedClients.get(t.getPlayers().get(i).getName()).refreshTable(tableID);
		}
	}
	
	/**
	 * Starts the next street at the table specified.
	 * @param tableID The table identifier
	 */
	@Override
	public void startNextStreet(int tableID)throws RemoteException{
		Table t = tables.get(tableID);
		t.dealFaceUp(true);
		t.setCallCount(0);
		determineFirstBetter(tableID, false);
		
		for (int i = 0; i < t.getPlayers().size(); i++) {
			connectedClients.get(t.getPlayers().get(i).getName()).refreshTable(tableID);
		}
	}
	
	/**
	 * Ends the round at the table specified.
	 * @param tableID The table identifier.
	 */
	@Override
	public void endRound(int tableID)throws RemoteException {
		Table t = tables.get(tableID);
		t.endRound(t.getWinners());
		if(!t.isEndEarly()){
			t.allFaceUp();
		}
		
		for (int i = 0; i < t.getPlayers().size(); i++) {
			connectedClients.get(t.getPlayers().get(i).getName()).refreshTable(tableID);
		}
	}
	
	/**
	 * Sends a message to the table specified.
	 * @param tableID The table identifier
	 * @param username The username of the user sending the message
	 * @param text The message being sent.
	 */
	public void sendMessage(int tableID, String username, String text) throws RemoteException {
		Table t = tables.get(tableID);
		t.addMessage(username, text);
		
		for (int i = 0; i < t.getPlayers().size(); i++) {
			connectedClients.get(t.getPlayers().get(i).getName()).refreshTable(tableID);
		}
	}
	
	/**
	 * Returns the list of messages at the table specified.
	 * @param tableID The table identifier.
	 * @return The list of strings representing what should be displayed in chat.
	 */
	public ArrayList<String> getMessages(int tableID) {
		return tables.get(tableID).getMessages();
	}
	
	/**
	 * Determines the first better at the table.
	 * @param tableID The table identifier
	 * @param firstRound True if the table is in the first street, false otherwise.
	 */
	@Override
	public void determineFirstBetter(int tableID, boolean firstRound) throws RemoteException{
		Table t = tables.get(tableID);
		Player p = t.determineFirstBetter(firstRound);
		t.setTurn(p);
		
	}
	
	/**
	 * Used to call with the current player at a table in the middle of a round.
	 * @param tableID The table identifier.
	 */
	@Override
	public void call(int tableID)throws RemoteException{
		Table t = tables.get(tableID);
		Player p = t.getCurrentTurn();
		p.call();
		t.incrementCallCount();
		if(t.getCallCount() >= t.getPlayers().size()-1){
			t.incrementStreetCount();
			if(t.getStreetCount() <= 4){
				t.resetPlayersCurrentBets();
				p.setActive();
				startNextStreet(tableID);
			}
			else{
				endRound(tableID);
			}
		}
		else{
			t.setNextTurn();
		}
		
		for (int i = 0; i < t.getPlayers().size(); i++) {
			connectedClients.get(t.getPlayers().get(i).getName()).refreshTable(tableID);
		}
	}
	
	/**
	 * Used to raise with the current player at a table during a round.
	 * @param tableID The table identifier
	 * @param bet The amount the user is raising.
	 */
	@Override
	public void raise(int tableID, int bet)throws RemoteException{
		Table t = tables.get(tableID);
		Player p = t.getCurrentTurn();
		p.raise(bet);
		t.setCallCount(0);
		t.setNextTurn();
		
		for (int i = 0; i < t.getPlayers().size(); i++) {
			connectedClients.get(t.getPlayers().get(i).getName()).refreshTable(tableID);
		}
	}
	
	/**
	 * Used to all-in the current player at a table during a round.
	 * @param tableID The table identifier
	 */
	@Override
	public void allIn(int tableID)throws RemoteException{
		Table t = tables.get(tableID);
		Player p = t.getCurrentTurn();
		p.allIn();
		t.setCallCount(0);
		t.setNextTurn();
		
		for (int i = 0; i < t.getPlayers().size(); i++) {
			connectedClients.get(t.getPlayers().get(i).getName()).refreshTable(tableID);
		}
	}

	/**
	 * Used to fold the current player at a table during a round.
	 * @param tableID 
	 */
	@Override
	public void fold(int tableID) throws RemoteException{
		Table t = tables.get(tableID);
		Player p = t.getCurrentTurn();
		int index = t.getPlayers().indexOf(p);
		p.fold();
		t.incrementCallCount();
		if(t.allFolded()){
			t.setEndEarly(true);
			endRound(tableID);
		}
		else{
			if(t.getCallCount() >= t.getPlayers().size()-1){
				t.incrementStreetCount();
				if(t.getStreetCount() <= 4){
					t.resetPlayersCurrentBets();
					startNextStreet(tableID);
				}
				else{
					endRound(tableID);
				}
			}
			else{
				t.setNextTurn(index);
			}
		}
		
		for (int i = 0; i < t.getPlayers().size(); i++) {
			connectedClients.get(t.getPlayers().get(i).getName()).refreshTable(tableID);
		}
	}

	/**
	 * Returns the current bet at a table.
	 * @param tableID The table identifier
	 * @return The current bet at the table
	 */
	@Override
	public int getCurrentBet(int tableID) throws RemoteException{
		Table t = tables.get(tableID);
		return t.getCurrentBet();
	}

	/**
	 * Removes a player from a table and withdraws his chips.
	 * @param tableID The table identifier
	 * @param username The username of the user leaving the table
	 * @param chipsWithdrawn The number of chips the user is taking from the table
	 */
	@Override
	public void removePlayerFromTable(int tableID, String username,	int chipsWithdrawn) throws RemoteException {
		Table t = tables.get(tableID);
		t.removePlayer(t.getPlayer(username));
		
		for (int i = 0; i < t.getPlayers().size(); i++) {
			connectedClients.get(t.getPlayers().get(i).getName()).refreshTable(tableID);
		}
	}

	/**
	 * Returns the TableInformation object of a table, giving the client most of what it needs to display the board
	 * @param tableID The table identifier
	 * @param username The username of the user requesting the TableInformation object
	 */
	@Override
	public TableInformation getTableInformation(int tableID, String username) throws RemoteException{
		Table tempTable = tables.get(tableID);
		TableSettings tempSettings = tempTable.getTableSettings();
		return new TableInformation(tempSettings.getMaxPlayers(), tempSettings.getMaxBet(), tempSettings.getAnte(), tempTable.getCurrentPot(), tempTable.getPlayers(), username, tempTable.isDone());
	}
	
}
